// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/phemex_base/api_util/rate_tracker.h"

#include <algorithm>
#include <sstream>
#include <string>

#include <boost/algorithm/string/replace.hpp>
#include <glog/logging.h>

#include "coin2/base/proto_util.h"
#include "coin2/exchange/base/order/rate_config.h"

namespace coin2::exchange::phemex::api_util {

constexpr auto EXAMPLE_SPOT_PHEMEX = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke18/phemex/trade_key.json",
  "rate_limit_info": {
    "spot_phemex": {
      "spotorder": {
        "secs": 60,
        "reqs": 500
      },
      "others": {
        "secs": 60,
        "reqs": 100
      }
    }
  }
},)";

constexpr auto EXAMPLE_FUTURES_PHEMEX = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke18/phemex/fut_trade_key.json",
  "rate_limit_info": {
    "futures_phemex": {
      "contract": {
        "secs": 60,
        "reqs": 500
      },
      "others": {
        "secs": 60,
        "reqs": 100
      }
    }
  }
},)";

template <class RateTracker>
void CheckFail(RateTracker&& rate_tracker, bool check_result) {
  if (!check_result) {
    LOG(INFO) << "Hit rate limit on [" << rate_tracker->GetName() << "]";
  }
}

PhemexRateTracker::PhemexRateTracker(
    const MarketExchangeApi& mea,
    const coin2::exchange::order::OrderConnectionConfig& cfg) {
  using RateConfig = coin2::exchange::base::order::RateConfig;
  if (mea.market == coin::proto::MarketType::Spot) {
    RateLimitInfoSpotPhemex rate_config;
    RateConfig::Get()->GetConfig(mea, cfg, &rate_config);
    InitRateTracker(&rate_config, mea.String(), EXAMPLE_SPOT_PHEMEX);
  } else if (mea.market == coin::proto::MarketType::Futures) {
    RateLimitInfoFuturesPhemex rate_config;
    RateConfig::Get()->GetConfig(mea, cfg, &rate_config);
    InitRateTracker(&rate_config, mea.String(), EXAMPLE_FUTURES_PHEMEX);
  } else {
    CHECK(false) << "unsupported mea: " << mea.String();
  }
}

void PhemexRateTracker::InitRateTracker(
    RateLimitInfoSpotPhemex* rate_config,
    std::string_view mea_str,
    std::string_view example) {
  if (!rate_config || !rate_config->has_spotorder() || !rate_config->has_others()) {
    coin2::exchange::base::order::RateConfig::CheckFail(mea_str, example);
  }
  const auto& spotorder = rate_config->spotorder();
  const auto& others = rate_config->others();
  LOG(INFO) << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            << "\nOG request rate was set as below:\n"
            << "\nspotorder: [" << spotorder.reqs() << "(reqs)/" << spotorder.secs() << "(seconds)]"
            << "\nothers: [" << others.reqs() << "(reqs)/" << others.secs() << "(seconds)]"
            << "\n\nOr you can configure customized rate in driver config. For example: " << example
            << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";

  rate_tracker_spotorder_ =
      std::make_unique<FixedWindowRateTracker>(spotorder.secs() * 1'000'000'000LL);
  rate_tracker_spotorder_->SetRateLimitInfo(spotorder.reqs(), spotorder.reqs());
  rate_tracker_spotorder_->SetName("phemex_rate_tracker_spotorder(1 min)");

  rate_tracker_others_ = std::make_unique<FixedWindowRateTracker>(others.secs() * 1'000'000'000LL);
  rate_tracker_others_->SetRateLimitInfo(others.reqs(), others.reqs());
  rate_tracker_others_->SetName("phemex_rate_tracker_others(1 min)");

  rate_limit_report_writer_ = std::make_unique<RateLimitReportWriter>();
  rate_limit_report_writer_->SetRateLimitInfoString(rate_config);
}

void PhemexRateTracker::InitRateTracker(
    RateLimitInfoFuturesPhemex* rate_config,
    std::string_view mea_str,
    std::string_view example) {
  if (!rate_config || !rate_config->has_contract() || !rate_config->has_others()) {
    coin2::exchange::base::order::RateConfig::CheckFail(mea_str, example);
  }
  const auto& contract = rate_config->contract();
  const auto& others = rate_config->others();
  LOG(INFO) << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            << "\nOG request rate was set as below:\n"
            << "\ncontract: [" << contract.reqs() << "(reqs)/" << contract.secs() << "(seconds)]"
            << "\nothers: [" << others.reqs() << "(reqs)/" << others.secs() << "(seconds)]"
            << "\n\nOr you can configure customized rate in driver config. For example: " << example
            << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";

  rate_tracker_contract_ =
      std::make_unique<FixedWindowRateTracker>(contract.secs() * 1'000'000'000LL);
  rate_tracker_contract_->SetRateLimitInfo(contract.reqs(), contract.reqs());
  rate_tracker_contract_->SetName("phemex_rate_tracker_contract(1 min)");

  rate_tracker_others_ = std::make_unique<FixedWindowRateTracker>(others.secs() * 1'000'000'000LL);
  rate_tracker_others_->SetRateLimitInfo(others.reqs(), others.reqs());
  rate_tracker_others_->SetName("phemex_rate_tracker_others(1 min)");

  rate_limit_report_writer_ = std::make_unique<RateLimitReportWriter>();
  rate_limit_report_writer_->SetRateLimitInfoString(rate_config);
}

bool PhemexRateTracker::HasEnoughRateLimit(int64_t timestamp, ActionType action_type) {
  if (CheckEnableTime(timestamp, action_type) && CheckRateLimit(timestamp, action_type)) {
    return true;
  }

  rate_limit_report_writer_->SetHitRateLimit(timestamp, action_type);
  return false;
}

void PhemexRateTracker::Push(int32_t num_reqs, int64_t timestamp, ActionType action_type) {
  if (action_type == ActionType::PLACE || action_type == ActionType::CANCEL ||
      action_type == ActionType::QUERY) {
    if (rate_tracker_spotorder_) {
      rate_tracker_spotorder_->Push(num_reqs, timestamp);
    }
  } else {
    CHECK(action_type == ActionType::UNKNOWN);
    if (rate_tracker_others_) {
      rate_tracker_others_->Push(num_reqs, timestamp);
    }
  }
}

void PhemexRateTracker::UpdateRateLimitSpotOrder(int32_t rate_limit_remain) {
  if (!rate_tracker_spotorder_) {
    return;
  }
  auto rate_limit = rate_tracker_spotorder_->MaxReq();
  rate_tracker_spotorder_->SetRateLimitInfo(rate_limit, rate_limit_remain);
  LOG_EVERY_NS(INFO, 5'000'000'000, GetCurrentTimestamp())
      << "update rate limit of spotOrder: max_req[" << rate_limit << "], remain["
      << rate_limit_remain << "], consumed[" << rate_limit - rate_limit_remain << "]";
}

void PhemexRateTracker::UpdateRateLimitContract(int32_t rate_limit_remain) {
  if (!rate_tracker_contract_) {
    return;
  }
  auto rate_limit = rate_tracker_contract_->MaxReq();
  rate_tracker_contract_->SetRateLimitInfo(rate_limit, rate_limit_remain);
  LOG_EVERY_NS(INFO, 5'000'000'000, GetCurrentTimestamp())
      << "update rate limit of contract: max_req[" << rate_limit << "], remain["
      << rate_limit_remain << "], consumed[" << rate_limit - rate_limit_remain << "]";
}

void PhemexRateTracker::UpdateRateLimitOthers(int32_t rate_limit_remain) {
  if (!rate_tracker_others_) {
    return;
  }
  auto rate_limit = rate_tracker_others_->MaxReq();
  rate_tracker_others_->SetRateLimitInfo(rate_limit, rate_limit_remain);
  LOG_EVERY_NS(INFO, 5'000'000'000, GetCurrentTimestamp())
      << "update rate limit of others: max_req[" << rate_limit << "], remain[" << rate_limit_remain
      << "], consumed[" << rate_limit - rate_limit_remain << "]";
}

void PhemexRateTracker::SetRateLimitInfoString(const google::protobuf::Message* message) {
  if (!rate_limit_report_writer_) {
    return;
  }
  rate_limit_report_writer_->SetRateLimitInfoString(message);
}

void PhemexRateTracker::WriteRateLimitReportMessage(OrderLoggerCB callback) {
  if (!callback) {
    return;
  }
  rate_limit_report_writer_->Write(callback);
}

double PhemexRateTracker::GetMaxRateUsedRatio(int64_t timestamp, ActionType action_type) {
  switch (action_type) {
    case ActionType::PLACE:
    case ActionType::CANCEL:
    case ActionType::QUERY: {
      double req_ratio = 0.;
      if (rate_tracker_spotorder_) {
        auto max_reqs = rate_tracker_spotorder_->MaxReq();
        auto remaining = rate_tracker_spotorder_->GetRemaining(timestamp);
        req_ratio = 1.0 * (max_reqs - remaining) / max_reqs;
      }
      return req_ratio;
    }
    case ActionType::UNKNOWN: {
      double req_ratio = 0.;
      if (rate_tracker_others_) {
        auto max_reqs = rate_tracker_others_->MaxReq();
        auto remaining = rate_tracker_others_->GetRemaining(timestamp);
        req_ratio = 1.0 * (max_reqs - remaining) / max_reqs;
      }
      return req_ratio;
    }
    default: {
      // shouldn't reached here
      LOG(ERROR) << "ops~ something is wrong, action_type=" << action_type;
      return 0.;
    }
  }
}

bool PhemexRateTracker::CheckEnableTime(int64_t timestamp, ActionType action_type) {
  if (action_type == ActionType::PLACE || action_type == ActionType::CANCEL ||
      action_type == ActionType::QUERY) {
    if (timestamp < enable_time_spotorder_) {
      LOG_EVERY_N(WARNING, 50) << "Hit spotorder api temporary disabled until "
                               << enable_time_spotorder_ << ", " << action_type;
      return false;
    }
    if (timestamp < enable_time_contract_) {
      LOG_EVERY_N(WARNING, 50) << "Hit contract api temporary disabled until "
                               << enable_time_contract_ << ", " << action_type;
      return false;
    }
  } else {
    CHECK(action_type == ActionType::UNKNOWN);
    if (timestamp < enable_time_others_) {
      LOG_EVERY_N(WARNING, 50) << "Hit others api temporary disabled until " << enable_time_others_
                               << ", " << action_type;
      return false;
    }
  }

  return true;
}
bool PhemexRateTracker::CheckRateLimit(int64_t timestamp, ActionType action_type) {
  bool ret = true;
  if (action_type == ActionType::PLACE || action_type == ActionType::CANCEL ||
      action_type == ActionType::QUERY) {
    if (rate_tracker_spotorder_) {
      ret = rate_tracker_spotorder_->GetRemaining(timestamp) > 2;
      CheckFail(rate_tracker_spotorder_, ret);
    }
  } else {
    CHECK(action_type == ActionType::UNKNOWN);
    if (rate_tracker_others_) {
      ret = rate_tracker_others_->GetRemaining(timestamp) > 2;
      CheckFail(rate_tracker_others_, ret);
    }
  }

  return ret;
}
}  // namespace coin2::exchange::phemex::api_util
